package org.ddevil.data.set;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import org.ddevil.data.Attribute;
import org.ddevil.data.BasicData;
import org.ddevil.data.Data;
import org.ddevil.data.Sparse2dArray;
import org.ddevil.data.StringPool;
import org.ddevil.data.util.DataSetIterator;


/**
 * An implementation of DataSet that uses Sparse2dArray and a string pool.
 * This can be used to save memory when results have a lot of nulls
 * or duplicate strings. This will be slower then BasicDataSet.
 *
 * @author Christopher Brady
 */
public class SparseDataSet implements DataSet {

    /**
	 *
	 */
	private static final long serialVersionUID = 1L;

	private static final String INVALID_DBID_EXCEPTION =
            "Invalid Database Identifier for this Data Set: ";

	/**
	 * The DB Id's for each piece of data in this data set.
	 */
	final private String[] dbIdentifiers;

	/**
	 * Maps each DB Id with it's index in our data backing array.
	 */
	private Hashtable<String, Integer> dbIdToIndexMap;

	/**
	 * The data in this data set.
	 */
	protected Sparse2dArray recordSet;

	/**
	 * Pool of strings that can be reused.
	 */
	protected transient StringPool stringPool;

    /**
     * This constructor will build a empty SparseDataSet with dbIdentifiers.
     */
	public SparseDataSet(final String[] dbIdentifiers, final StringPool stringPool) {
		this.dbIdentifiers = dbIdentifiers;
        buildIdToIndexMap();

        this.stringPool = stringPool;

        recordSet = new Sparse2dArray(stringPool, dbIdentifiers.length);
        recordSet.trimToSize();
	}

    /**
     * This constructor will build a SparseDataSet from attributes.
     */
	public SparseDataSet(final Data attributes, final StringPool stringPool) {

		// Set the string pool
        this.stringPool = stringPool;

        // Get the DB IDs
        String[] dbIds = new String[attributes.size()];

        int i = 0;
        for(Attribute di : attributes){
        	dbIds[i++] = stringPool.getString(di.getId());
        }

		this.dbIdentifiers = dbIds;
        buildIdToIndexMap();

        // Init the record set
        recordSet = new Sparse2dArray(stringPool, dbIdentifiers.length);

        // Set the data
        importRecord(attributes);
        recordSet.trimToSize();
	}

    /**
     * This constructor will build a SparseDataSet from a {@link ResultSet} and
     * a string pool.
     * The meta data will be used to get the db identifiers and each row
     * from the result set will be added to this data set.
     * This constructor does not handle any SQLExceptions that may occur during
     * this process.
     *
     * @throws SQLException
     */
    public SparseDataSet(final ResultSet resultSet, final StringPool stringPool) throws SQLException {

    	this.stringPool = stringPool;

        // Get the column ids from the meta data
        ResultSetMetaData meta = resultSet.getMetaData();
        int columnCount = meta.getColumnCount();

        String[] columnIds = new String[columnCount];

        for(int i = 0; i < columnIds.length; i++){
        	String colName = meta.getColumnName(i+1);
        	columnIds[i] = stringPool.getString(colName);
        }

        // Set db ids and init our index lookup table
        this.dbIdentifiers = columnIds;
        buildIdToIndexMap();


        // Add the data
        recordSet = new Sparse2dArray(stringPool, columnCount);
        while(resultSet.next()){

            ArrayList row = new ArrayList(columnCount);
            for(int i = 0; i < columnCount; i++){
            	row.add(resultSet.getObject(i+1));
            }
            recordSet.add(row);
        }
        recordSet.trimToSize();
    }

    /**
     * When we set our db identifiers we also create this map for
     * faster column access using db ids.
     */
    private void buildIdToIndexMap(){
        /* set db ids and init our index lookup table */
        dbIdToIndexMap = new Hashtable<String, Integer>(dbIdentifiers.length);

        for (int i = 0; i < dbIdentifiers.length; i++) {
            dbIdToIndexMap.put(dbIdentifiers[i], i);
        }
    }

	/**
	 * Set the data in this data set to the given data.
	 *
	 * @param records
	 */
	public synchronized void setData(final Object[][] data) {
		// Clear the current record set
		recordSet.clear();

		// Change each row to an ArrayList and add it to the recordSet
		for(Object[] row : data) {
			ArrayList addRow = new ArrayList(row.length);

			for(Object a : row) {
				addRow.add(a);
			}
			recordSet.add(addRow);
		}
	}

	/**
	 * Export a {@link Data} object for the row with the given index.
	 * <p>
	 * Modifications to the returned object should NOT be reflected in this Data
	 * Set.
	 *
	 * @param rowNum -
	 *            The index of the row in our data set.
	 * @return - A Record object that describes the row at the given index.
	 * @throws IndexOutOfBoundsException
	 *             If any of the indices > size()
	 */
	public synchronized Data exportRecord(final int rowNum) {

		ArrayList record = recordSet.getData(rowNum);
        BasicData returnValue = new BasicData(dbIdentifiers, record.toArray());

		return returnValue;
	}

	/**
	 * This is used by exportRecords.
	 */
	protected void setDataForExport(final ArrayList in) {
		recordSet.add(in);
	}

	/**
	 * Return a DataSetInterface subset of this data set containing each of the
	 * records specified by the passed in indices.
	 * <p>
	 * Modifications to the returned object should NOT be reflected in this Data
	 * Set.
	 *
	 * @param indices
	 *            The row indices of the records to export.
	 * @return A {@link DataSetInterface} of records corresponding to the given
	 *         indices.
	 * @throws IndexOutOfBoundsException
	 *             If any of the indices > size()
	 */
	public synchronized DataSet exportRecords(final int... indices) {

		SparseDataSet returnValue = new SparseDataSet(dbIdentifiers, new StringPool(5000));

		for (int indice : indices) {

			returnValue.setDataForExport(recordSet.getData(indice));
		}

		return null;
	}

	/**
	 * Return the data in the given row with the given DB Id.
	 * <p>
	 * If the DB Id doesn't exist for this Data Set a null value will be
	 * returned.
	 *
	 * @param rowIndex
	 *            The index of the row.
	 * @param dbIdentifier
	 *            The DB Id of the actual piece of data in the row.
	 * @return The value at that location.
	 * @throws IndexOutOfBoundsException
	 *             If rowIndex > size()
	 */
	public synchronized Object getDataAt(final int rowIndex, final String dbIdentifier) {
        int colIndex = dbIdToIndexMap.get(dbIdentifier);

        Object returnValue = recordSet.getData(rowIndex, colIndex);

		return returnValue;
	}

	/**
	 * Attempt to import this criteria into our data set.
	 * <p>
	 * Any attributes in the inData object that are not supported by this data
	 * set will be dropped.<br>
	 * Any attributes that this data set supports that are not contained in the
	 * inData object will be filled in with null values.
	 *
	 * @param inData
	 *            A {@link Data} object giving the data to add.
	 */
	public synchronized void importRecord(final Data inData) {
		// TODO Auto-generated method stub
		ArrayList toAdd = new ArrayList(dbIdentifiers.length);

		for(String dbId : dbIdentifiers) {
			if(inData.contains(dbId)) {
				Object value = inData.getValue(dbId);
				if(value instanceof String) {
					if(stringPool != null) {
						value = stringPool.getString((String)value);
					}
				}
				toAdd.add(value);
			} else {
				toAdd.add(null);
			}
		}

		recordSet.add(toAdd);
	}

	/**
	 * Attempt to import the data into this data set. Each record in the inData
	 * object will be added, in order, to the end of this data set.
	 * <p>
	 * Any attributes in the inData object that are not supported by this data
	 * set will be dropped.<br>
	 * Any attributes that this data set supports that are not contained in the
	 * inData object will be filled in with null values.
	 *
	 * @param data
	 *            The data to add.
	 */
	public synchronized void importRecords(final DataSet inData) {

		int size = inData.size();
		for(int i=0; i<size; i++) {
			Data rec = inData.exportRecord(i);
			importRecord(rec);
		}
	}

	/**
	 * Set the value of the data that is in the given row and id'd with the
	 * given DB Id.
	 *
	 * @param rowIndex
	 *            The row where the data will be set.
	 * @param dbIdentifier
	 *            The DB Id of the actual piece of data in the row.
	 * @param data
	 *            The new data value.
	 * @throws IndexOutOfBoundsException
	 *             If rowIndex > size()
	 * @throws IllegalArgumentException
	 *             If the dbIdentifier is invalid for this Data Set.
	 */
	public synchronized void setDataAt(final int rowIndex, final String dbIdentifier, final Object data) {
        Integer colIndex = dbIdToIndexMap.get(dbIdentifier);

        if( colIndex == null ) {
            throw new IllegalArgumentException(INVALID_DBID_EXCEPTION + dbIdentifier);
        }

		recordSet.setData(rowIndex, colIndex, data);
	}

	/**
	 * Remove the record at the given index from the data set. All records after
	 * this record will have their index shifted down by one.
	 * <p>
	 *
	 * @param index
	 *            The index of the data row.
	 * @throws IndexOutOfBoundsException
	 *             If rowIndex > size()
	 */
	public synchronized void removeRecord(final int index) {
		recordSet.remove(index);

	}

	/**
	 * Clear all data from this dataset. The set of DB Id's that define this
	 * data set will remain intact.
	 */
	public synchronized void clear() {
		recordSet.clear();
	}

	/**
	 * Get all the column Identifiers for this data set.
	 * <p>
	 * Modifications to the returned object should NOT be reflected in the Data
	 * Set.
	 *
	 * @return The column IDs.
	 */
	public synchronized List<String> getIdentifiers() {
        ArrayList<String> returnValue = new ArrayList<String>();

        for(String dbId : dbIdentifiers) {
			returnValue.add(dbId);
		}

        return returnValue;
	}

	/**
	 * Get the number of data rows in the data set.
	 *
	 * @return - The size of our data set.
	 */
	public synchronized int size() {
		return recordSet.getRowCount();
	}

	public Iterator iterator() {
		return new DataSetIterator<Data>(this);
	}

}
